/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package base.node;

import java.awt.event.ActionEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import base.message.Message;
import base.message.MessageOriginBean;
import base.server.ServingThread;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.*;
import java.util.Date;
import javax.swing.Timer;
import networking.SocketMessageFactory;
import routing.RouteNodeAbstraction;
import simulation.GUIOptionManager;

/**
 * Class representing the node's functionalities and information
 * @author artem, Arvanitis Ioannis
 */
public abstract class Node extends Thread implements Serializable {

    private int portOffset; //communication server port
    private Socket socket;	//the transmitter
    private ObjectInputStream ois;
    private ObjectOutputStream oos;
    private boolean radioOn; //radio enabled
    private PowerPolicy powerPolicy; //power details
    private NodeInfo nodeInfo; //id, energy & position
    private int transmissionCount; //number of messages sent
    private Object obj;
    private boolean connected;
    private Timer timer4IdleConsumption;
    private Timer timer4InternalOperations;
    private final int ONE_SEC = 1000;
    private final int ONE_HOUR = 3600 * ONE_SEC / GUIOptionManager.getTimeScale();

    /**
     * Gets the node's info
     * @return the node's info
     */
    public NodeInfo getNodeInfo() {
        return nodeInfo;
    }

    /**
     * Sets the node's info
     * @param nodeInfo
     */
    public void setNodeInfo(NodeInfo nodeInfo) {
        this.nodeInfo = nodeInfo;
    }

    /**
     * Determines whether the radio is on or not
     * @return whether the radio is on or not
     */
    public boolean isRadioOn() {
        return radioOn;
    }

    /**
     * Sets whether the radio is on or not
     * @param isRadioOn Whether the radio is on or not
     */
    public void setRadioOn(boolean isRadioOn) {
        this.radioOn = isRadioOn;
    }

    /**
     * Gets the current number of total messages sent
     * @return the current number of total messages sent
     */
    public int getTransmissionCount() {
        return transmissionCount;
    }

    /**
     * Sets the current number of total messages sent
     * @param transmissionCount The current number of total messages sent
     */
    public void setTransmissionCount(int transmissionCount) {
        this.transmissionCount = transmissionCount;
    }

    /**
     * Gets the power policy for this node
     * @return the power policy for this node
     */
    public PowerPolicy getPowerPolicy() {
        return powerPolicy;
    }

    /**
     * Sets the power policy for this node
     * @param powerPolicy The power policy for this node
     */
    public void setPowerPolicy(PowerPolicy powerPolicy) {
        this.powerPolicy = powerPolicy;
    }

    /**
     * Gets the timer for idle consumption
     * @return the timer for idle consumption
     */
    public Timer getTimer4IdleConsumption() {
        return timer4IdleConsumption;
    }

    /**
     * Gets the timer for internal operations
     * @return the timer for internal operations
     */
    public Timer getTimer4InternalOperations() {
        return timer4InternalOperations;
    }

    public Node(int portOffset, NodeInfo nodeInfo, PowerPolicy pp) {
        setNodeInfo(nodeInfo);
        setPowerPolicy(pp);
        this.portOffset = portOffset;
        print("Node " + nodeInfo.getId() + " initialized successfully");
    }

    @Override
    public void run() {
        setRadioOn(true);

        timer4IdleConsumption = new Timer(ONE_HOUR, new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                reduceEnergy(getPowerPolicy().getIdleEnergyConsumption());
            }
        });
        timer4InternalOperations = new Timer(getNodeInfo().getDaemonInterval() * ONE_SEC, new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                //perform internal operations//
                //check if mobile and move if so
                if (GUIOptionManager.getNodeArchPerCategory().get(Integer.parseInt(GUIOptionManager.getNodes().get(getNodeInfo().getId() - 1).getInfo()) - 1).isMobile()) {
                    Daemon daemon = new Daemon(getNodeInfo().getId());
                    daemon.move();
                }
            }
        });
        timer4IdleConsumption.start();
        timer4InternalOperations.start();

        try {
            socket = new Socket(GUIOptionManager.getIp(), portOffset + nodeInfo.getId());
            connected = true;
            print("Node " + nodeInfo.getId() + " listening in port " + (portOffset + nodeInfo.getId()));
            oos = new ObjectOutputStream(socket.getOutputStream());
            oos.flush();
            ois = new ObjectInputStream(socket.getInputStream());
            while (connected) {
                obj = ois.readObject();
                if (obj instanceof Message) {
                    Message msg = (Message) obj;
                    manageMessage(msg);
                }
                else if (obj instanceof SocketMessageFactory) {
                    SocketMessageFactory smf = (SocketMessageFactory) obj;
                     if (smf.getMsgType().equals("CONNECTION_CLOSE")) {
                         smf = new SocketMessageFactory();
                         smf.setMsgType("CONNECTION_CLOSE");
                         send(smf);
                         connected = false;
                     }
                }
            }
        } catch (IOException ioe) {
            Logger.getLogger(Node.class.getName()).log(Level.SEVERE, null, ioe);
        } catch (ClassNotFoundException cnfe) {
            Logger.getLogger(Node.class.getName()).log(Level.SEVERE, null, cnfe);
        } finally {
            try {
                socket.close();
                print("Node " + nodeInfo.getId() + " closing down...");
            } catch (IOException ioe) {
                Logger.getLogger(ServingThread.class.getName()).log(Level.SEVERE, null, ioe);
            }
        }
    }

    /**
     * Sends a SocketMessageFactory instance
     * @param smf A SocketMessageFactory instance
     */
    public void send(SocketMessageFactory smf) {
        try {
            oos.writeObject(smf);
        } catch (IOException ex) {
            Logger.getLogger(Node.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * The main sending function.
     * Sub-sending functions may be implemented for ease of maintainance
     * @param msg The message to be sent
     */
    public void send(Message msg, boolean isSource) {
        boolean msgSent = false;
        setTransmissionCount(getTransmissionCount() + 1);
        if (isSource) {
            msg.setSrcInfo(new MessageOriginBean(getNodeInfo().getId(), getTransmissionCount(), new Date().getTime()));
        }
        msg.setTransmitterInfo(new MessageOriginBean(getNodeInfo().getId(), getTransmissionCount(), new Date().getTime()));
        try {
            oos.writeObject(msg);
            msgSent = true;
        } catch (IOException ioe) {
            Logger.getLogger(Node.class.getName()).log(Level.SEVERE, null, ioe);
            msgSent = false;
        } finally {
            reduceEnergy(getPowerPolicy().getTransmissionEnergyConsumption());
            if (msgSent) {
                print("Message " + msg.getTransmitterInfo().getMessageId() + " was successfully sent");
//			if (msg.getTargetNode() > -1) {
//				print("Successfully sent message to node " + msg.getTargetNode() + " transmission id = " + String.valueOf(getTransmissionCount()));
//			} else {
//				print("Successfully broadcasted message, transmission id = " + String.valueOf(getTransmissionCount()));
//			}
            } else {
                print("Failed to send message " + msg.getTransmitterInfo().getMessageId());
            }
        }
    }

    /**
     * Manages the message received appropriately
     * @param msg The message received
     */
    private void manageMessage(Message msg) {
        print("Message received from node " + msg.getTransmitterInfo().getNodeId() + " with id " + msg.getTransmitterInfo().getMessageId() + ", message type " + msg.getMessageType() + ", ttl " + msg.getTime2live() + ", data " + msg.getData().getServiceData().get(0));

        //check acceptance / rejection

        if (msg.getTargetNodes().isNodeinArrayList(getNodeInfo().getId())) {
            //accept to process it and reduce the consumed energy for reception
            reduceEnergy(getPowerPolicy().getReceptionEnergy());
            delay(GUIOptionManager.getTimeScaleTranception());
            //check TTL and reduce it by one
            if (msg.getTime2live() > 0) {
                msg.setTime2live(msg.getTime2live() - 1);
            }
            //update the routenodelist
            RouteNodeAbstraction rna = msg.getTargetNodes().getSubRouteNodeList(getNodeInfo().getId());
            //set the new targetNodes if there are such and process it
            msg.setTargetNodes(rna.getForwardNodeList());
            //check if this node is a target node and process the message if so
            if (rna.isTarget()) {
                //process the data
                reduceEnergy(getPowerPolicy().getProcessEnergy());
                delay(GUIOptionManager.getTimeScaleProcessing());
                print(msg.getData().getServiceData().get(0));
            }
            //forward the message if needed
            if (msg.getTargetNodes() != null) {
                //forward the message furthermore
                send(msg, false);
                delay(GUIOptionManager.getTimeScaleTranception());
            }
        } else {
            //simply reject it and reduce the consumed energy for reception
            reduceEnergy(getPowerPolicy().getReceptionEnergy());
            delay(GUIOptionManager.getTimeScaleTranception());
        }
    }

    private void delay(int delay) {
        try {
            int time = delay * ONE_SEC * ONE_SEC * ONE_SEC / GUIOptionManager.getTimeScale();
            sleep(time / (ONE_SEC * ONE_SEC), time % (ONE_SEC * ONE_SEC));
        } catch (InterruptedException ex) {
            Logger.getLogger(Node.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     *
     * @param msg
     * Checks whether the node who received the message is target
     * @return
     */
//	protected boolean isNodeTarget(Message msg) {
//		return msg.getTargetNodes().get(0).isTarget();
//	}
    /**
     * Checks whether there is a forward list for the node that received the message
     * @param msg, the message to be passed in the function
     * @return the boolean variable that shows if the forwardlist of the node is full
     */
//	protected boolean isForwardListFull(Message msg) {
//		boolean forwardListFull = true;
//		if (msg.getTargetNodes().get(0).getForwardNodeList() == null) {
//			forwardListFull = false;
//		}
//		return forwardListFull;
//	}
    /**
     * fixes the rnl of the received message so as to be ready to send the message to the next node in the array list
     * @return the newArraylist
     */
//	public RouteNodeList updateArrayList(Message msg) {
//		RouteNodeList newArrayList = msg.getTargetNodes().get(0).getForwardNodeList();
//		return newArrayList;
//	}
    /**
     * The function processes the inserted message in the sense of fixing the hopCount and the targetNodes rnl
     * @param msg, the message to be inserted in the function so as to be processed
     * @return the fixed message, ready to be sent
     */
//	public Message processReceivedMessage(Message msg) {
//		msg.setHopCount(msg.increaseHopCount());
//		msg.setTargetNodes(updateArrayList(msg));
//		return msg;
//	}
    /**
     * Abstraction function to check whether a node is neighbour to the current node
     * @param arraylist The neighbour node list to check
     * @param id The node id of the node to check
     * @return true if the node with the specified id is indeed a neighbouring node
     */
//	protected boolean isNodeNeighbour(ArrayList<Integer> arraylist, int id){
//		return isNodeinArrayList(arraylist, id);
//	}
    /**
     * The function finds whether or not a node (its id) exist in a specified rnl
     * @param rnl, an rnl for storing the neighbours' ids
     * @param id, an integer of the id of neighbour
     * @return a boolean variable for checking if the id of the neighbour is already in the rnl
     */
//	private boolean isNodeinArrayList(ArrayList<Integer> arraylist, int id) {
//		boolean found = false;
//		for (int i = 0; i < arraylist.size(); i++) {
//			if (arraylist.get(i) == id) {
//				found = true;
//				break;
//			}
//		}
//		return found;
//	}
    /**
     * Updates the ArrayList of neighbours of the node
     * @param msg, accepts a Message as a parameter, for fixing the neighbours rnl
     */
//	protected void updateNeighboursArrayList(Message msg) {
//		int searchId = msg.getData().getNodeInfo().getId();
//		if (!(isNodeinArrayList(nodeNeighbourList, searchId))) {
//			nodeNeighbourList.add(searchId);
//		}
//		print("Neighbour list updated. Current neighbour list is:");
//		for(int i:nodeNeighbourList){
//			print(String.valueOf(i));
//		}
//		return;
//	}
    /**
     * A method that updates the list of neighbours the node maintains
     */
//	public void checkForNeighbours() {
//		Data data = new Data(0, 0, this.nodeInfo);
//		Message tmp = new Message(new RouteNodeList(1).updateRouteNodeList(new RouteNodeAbstraction(-1, true, null)),msgTypes.REQUEST_FOR_NODE_NEIGHBOUR, 1024, data, 0, generateMessageId());
//		send(tmp);
//	}
    /**
     * A function that relays an incoming message upon request
     * @param message The message to be relayed
     */
//	protected void relayMessage(Message message) {
//		RouteNodeList targets = message.getTargetNodes();
//		message.setTargetNodes(targets.get(0).getForwardNodeList());
//		message.setData(new Data(message.getData().getServiceId(), message.getData().getValue(), nodeInfo));
//		send(message);
//		return;
//	}
    /**
     * Reduces the energy amount based on the reduction indicated
     * @param reduction The energy reduction based on the reduction indicated
     */
    protected void reduceEnergy(float reduction) {
        getNodeInfo().setEnergy(getNodeInfo().getEnergy() - reduction);
        print("Current energy is " + getNodeInfo().getEnergy());
        if (getNodeInfo().getEnergy() < 20) {
            setRadioOn(false);
            print("Low energy, shutting down radio");
        }
    }

    /**
     * Prints custom node messages
     * @param s The string to be printed
     */
    protected void print(String s) {
        Date date = new Date();
        System.out.println(" ::NODE:: " + date.getTime() + "\tNode " + nodeInfo.getId() + "\t" + s);
    }
}
